<?php

declare(strict_types=1);

namespace App\Utils;

use Illuminate\Support\Facades\Redis;

/**
 * 全局redis键值配置
 */
class RedisUtil
{
    private $Key;

    private        $Redis;
    private static $instances;

    public function __construct()
    {
        $this->Redis = Redis::connection();
    }

    /**
     * @param string $key
     * @param string ...$dynamic key动态值拼接
     * @return self
     */
    public static function getInstance(string $key, string ...$dynamic)
    {
        $dynamics = '';
        foreach ($dynamic as $k => $v) {
            if ($k == 0) {
                $dynamics .= $v;
            } else {
                $dynamics .= '_' . $v;
            }
        }

        if (!isset(self::$instances)) {
            self::$instances = new self();
        }
        self::$instances->Key = $key . $dynamics;
        return self::$instances;
    }

    /**
     * 生成唯一标识
     * @param int $length 生成长度
     * @return string
     */
    public function createNo(int $length = 5): string
    {
        $redis = $this->Redis;

        $key = $this->Key;

        //指定键值新增+1 并获取
        $reqNo = $redis->incr($key);

        //值长度已达到5位，且首位已为9，初始化
        if (strlen($reqNo) >= $length && substr($reqNo, 0, 1) == 9) {
            $redis->set($key, 1);
            $reqNo = 1;
        }

        if ($reqNo == 1) {
            //设置月末到期
            $expire = strtotime(date('Y-m', strtotime("+1 month")) . '-01 00:00:00') - time();
            $redis->expire($key, $expire);
        }

        return sprintf("%0{$length}d", $reqNo);
    }

    /**
     * 限制接口请求次数
     * @param string $path 可以是路由，可以是地址，可以是ip，总之是 string 即可
     * @param int $second 秒
     * @param int $count 多长时间内限制请求次数
     * @return bool
     */
    public function requestLimit(string $path, int $second, int $count): bool
    {
        $redis = $this->Redis;

        $key = $this->Key . md5($path);

        /*$_cou  = $redis->incr($key);
        if ($_cou > $count) return false;
        if ($_cou == 1) $redis->expire($key, $second);
        unset($redis, $_cou);
        return true;*/
        $luaScript = <<<lua
            local count = redis.call("incr",KEYS[1])
            if count == 1 then
              redis.call('expire',KEYS[1],ARGV[2])
            end
            if count > tonumber(ARGV[1]) then
              return 0
            end
              return 1
            lua;
        return (bool)$redis->eval($luaScript, 1, $key, $count, $second);
    }

    /**
     * 限制尝试次数 （设定时间内）
     * @param int $second 秒
     * @param int $Times 次数
     * @return false|int
     */
    public function tryTimes(int $second, int $Times = 5)
    {
        $redis = $this->Redis;

        $key = $this->Key;

        //指定键值新增+1 并获取
        $count = $redis->incr($key);
        if ($count > $Times) {
            return false;
        }

        //设置过期时间
        if ($count == 1) {
            $redis->expire($key, $second);
        }

        return (int)$count;
    }

    /**
     * 加锁
     * @param int $timeout 锁的过期时间
     * @return false|string
     */
    public function Lock(int $timeout = 10)
    {
        $redis    = $this->Redis;
        $lockName = $this->Key;//锁的名字

        #获取唯一标识符
        $identifier = uniqid();

        #锁的过期时间
        $end = time() + $timeout;

        #循环获取锁
        while (time() < $end) {
            #查看$lockName是否被上锁,为$lockName设置过期时间，防止死锁
            if ($redis->set($lockName, $identifier, 'ex', $timeout, 'nx')) {
                return $identifier;
            }

            #停止0.001ms
            usleep(0.001);
        }
        return false;
    }

    /**
     * 释放锁
     * @param string $identifier 锁的唯一值
     * @return bool
     */
    public function releaseLock(string $identifier): bool
    {
        $redis    = $this->Redis;
        $lockName = $this->Key;//锁的名字

        // 判断是锁有没有被其他客户端修改
        if ($redis->get($lockName) != $identifier) {
            #其他客户端修改了锁，不能删除别人的锁
            return false;
        }

        #释放锁
        $redis->del($lockName);
        return true;
    }

    public function setex(string $value, int $second)
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->setex($key, $second, $value);
    }

    public function get()
    {
        $redis = $this->Redis;
        $key   = $this->Key;

        return $redis->get($key);
    }

    public function del()
    {
        $redis = $this->Redis;
        $key   = $this->Key;

        return $redis->del($key);
    }

    //加入list
    public function lPush(array $value)
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->lPush($key, $value);
    }

    //删除右边
    public function rPop()
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->rPop($key);
    }

    //list长度
    public function lLen()
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->llen($key);
    }

    public function zadd($score, $member)
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->zadd($key, $score, $member);
    }

    public function zcard()
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->zcard($key);
    }

    public function zrem($member)
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->zrem($key, $member);
    }

    // 递增返回
    public function zRange($start_score = 0, $end_score = -1)
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->zrange($key, $start_score, $end_score);
    }

    // 递减返回
    public function zRevRange($start_score = 0, $end_score = -1)
    {
        $redis = $this->Redis;

        $key = $this->Key;

        return $redis->zRevRange($key, $start_score, $end_score);
    }
}